/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package protoson;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.google.common.io.BaseEncoding;
import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.EnumDescriptor;
import com.google.protobuf.Descriptors.EnumValueDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.DynamicMessage;
import com.google.protobuf.DynamicMessage.Builder;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.List;

/**
 * @author rehak
 */
public class GPBUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(GPBUtils.class);
    private static ClassLoader cls = ClassLoader.getSystemClassLoader();
    private static final ObjectMapper mapper = new ObjectMapper();

    static {
        mapper.configure(SerializationFeature.CLOSE_CLOSEABLE.INDENT_OUTPUT, true);
    }

    public static String gpb2json(Message msg) throws Exception {
        String json = MyJsonFormat.printToString(msg);
        return json;
    }

    public static <T extends Message> T json2gpb(String json, T msg) throws IOException, InvalidProtocolBufferException {
        JsonNode rootNode = mapper.readValue(json, JsonNode.class);
        DynamicMessage createGPBFromJSON = json2gpb(rootNode, msg.getDescriptorForType());
        Message res = msg.getParserForType().parseFrom(createGPBFromJSON.toByteArray());
        return (T) res;
    }

    public static ClassLoader getCls() {
        return cls;
    }

    public static void setCls(ClassLoader cls) {
        GPBUtils.cls = cls;
    }

    private static Message getDefaultInstanceOfMessage(String fullyQualifiedClassName) {
        String className = fullyQualifiedClassName;
        try {
            LOGGER.debug("Looking for: " + className);
            Class<?> c = Class.forName(className, true, cls);
            LOGGER.debug("Found: " + c);
            Method[] allMethods = c.getDeclaredMethods();
            for (Method m : allMethods) {
                if (m.getName().equals("getDefaultInstance")) {
                    LOGGER.debug("Found factory method: " + m.getName());
                    Object[] args = null;
                    Message inst = (Message) m.invoke(null, args);
                    LOGGER.debug("Got default instance: " + inst);
                    return inst;
                }
            }
        } catch (IllegalAccessException ex) {
            LOGGER.error("exception while getting descriptor", ex);
        } catch (IllegalArgumentException ex) {
            LOGGER.error("exception while getting descriptor", ex);
        } catch (InvocationTargetException ex) {
            LOGGER.error("exception while getting descriptor", ex);
        } catch (ClassNotFoundException ex) {
            LOGGER.error("exception while getting descriptor", ex);
        }
        return null;
    }

    private static Message.Builder getBuilderForClass(String fullyQualifiedClassName) {
        Message inst = getDefaultInstanceOfMessage(fullyQualifiedClassName);
        return inst.newBuilderForType();
    }

    private static Descriptor getDescriptorForClass(String fullyQualifiedClassName) {
        Message inst = getDefaultInstanceOfMessage(fullyQualifiedClassName);
        return inst.getDescriptorForType();
    }

    private static void setRequiredField(Builder b, FieldDescriptor fd, JsonNode json) throws IOException {
        String name = fd.getName();
        JsonNode jsonObject = null;
        if (json.has(name)) {
            jsonObject = json.get(name);
            setField(jsonObject, fd, b, false);
        } else {
            throw new IllegalArgumentException("Required field " + fd.getName() + " not found!");
        }
    }

    private static void setOptionalField(Builder b, FieldDescriptor fd, JsonNode json) throws IOException {
        String name = fd.getName();
        JsonNode jsonObject;
        if (json.has(name)) {
            jsonObject = json.get(name);
            setField(jsonObject, fd, b, false);
        }
    }

    private static void setRepeatedField(Builder b, FieldDescriptor fd, JsonNode json) throws IOException {
        String name = fd.getName();
        JsonNode jsonObject = null;
        if (json.has(name)) {
            jsonObject = json.get(name);
        } else {
            // not set
            return;
        }
        if (jsonObject.isArray()) {
            Iterator<JsonNode> elements = jsonObject.iterator();
            int cnt = 0;
            while (elements.hasNext()) {
                JsonNode e = elements.next();
                setField(e, fd, b, true);
                cnt++;
            }
        } else {
            throw new IllegalArgumentException("Repeated field - array expected");
        }
    }

    private static void setField(JsonNode node, FieldDescriptor fd, Builder b, boolean repeated) throws IOException {
        try {
            Object add;
            switch (fd.getJavaType()) {
                case BYTE_STRING:
                    // decode hex
                    String ap = node.asText();
                    byte[] data;
                    data = BaseEncoding.base64().decode(ap);
                    add = ByteString.copyFrom(data);
                    break;
                case INT:
                    add = node.asInt();
                    break;
                case LONG:
                    add = node.asLong();
                    break;
                case FLOAT:
                    add = new Double(node.asDouble()).floatValue();
                    break;
                case DOUBLE:
                    add = node.asDouble();
                    break;
                case STRING:
                    add = node.asText();
                    break;
                case BOOLEAN:
                    add = node.asBoolean();
                    break;
                case ENUM:
                    add = node.asText();
                    EnumDescriptor ed = fd.getEnumType();
                    EnumValueDescriptor evd = ed.findValueByName((String) add);
                    add = evd;
                    break;
                case MESSAGE:
                    add = json2gpb(node, fd.getMessageType());
                    break;
                default:
                    throw new IllegalArgumentException("Not recognized - " + node);
            }
            if (repeated) {
                b.addRepeatedField(fd, add);
            } else {
                b.setField(fd, add);
            }
        } catch (Exception e) {
            LOGGER.error("Error setting field " + fd.getFullName() + " for node " + node + ", repeated: " + repeated);
            throw e;
        }
    }

    private static void setField(Builder b, FieldDescriptor fd, JsonNode json) throws IOException {
        // must be present if is required        
        if (fd.isRequired()) {
            setRequiredField(b, fd, json);
        }
        // is present 0 or 1 time
        if (fd.isOptional()) {
            setOptionalField(b, fd, json);
        }
        // repeated (0-xxx occurance)
        if (fd.isRepeated()) {
            setRepeatedField(b, fd, json);
        }
    }

    public static JsonNode parseJSON(String jsonString) throws IOException {
        // general method, same as with data binding
        ObjectMapper mapper = new ObjectMapper();
        // (note: can also use more specific type, like ArrayNode or ObjectNode!)
        JsonNode rootNode = mapper.readValue(jsonString, JsonNode.class);
        return rootNode;
    }

    public static DynamicMessage json2gpb(JsonNode json, Descriptor desc) throws IOException {
        Builder builder = DynamicMessage.newBuilder(desc);
        List<FieldDescriptor> fields = desc.getFields();
        // go through fields
        for (FieldDescriptor fd : fields) {
            setField(builder, fd, json);
        }
        return builder.build();
    }
}
